En omfattende guide til TypeScript's kraftfulde Mapped Types og Conditional Types, inklusiv praktiske eksempler og avancerede anvendelser til at skabe robuste og typesikre applikationer.
Mestring af TypeScript's Mapped Types og Conditional Types
TypeScript, et supersæt af JavaScript, tilbyder kraftfulde funktioner til at skabe robuste og vedligeholdelsesvenlige applikationer. Blandt disse funktioner skiller Mapped Types og Conditional Types sig ud som essentielle værktøjer til avanceret typemanipulation. Denne guide giver en omfattende oversigt over disse koncepter, udforsker deres syntaks, praktiske anvendelser og avancerede use cases. Uanset om du er en erfaren TypeScript-udvikler eller lige er startet på din rejse, vil denne artikel udstyre dig med den viden, der skal til for at udnytte disse funktioner effektivt.
Hvad er Mapped Types?
Mapped Types giver dig mulighed for at oprette nye typer ved at transformere eksisterende. De itererer over egenskaberne i en eksisterende type og anvender en transformation på hver egenskab. Dette er især nyttigt til at skabe variationer af eksisterende typer, såsom at gøre alle egenskaber valgfri eller skrivebeskyttede.
Grundlæggende syntaks
Syntaksen for en Mapped Type er som følger:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T: Input-typen, du vil mappe over.K in keyof T: Itererer over hver nøgle i input-typenT.keyof Tskaber en union af alle egenskabsnavne iT, ogKrepræsenterer hver enkelt nøgle under iterationen.Transformation: Den transformation, du vil anvende på hver egenskab. Dette kan være at tilføje en modifikator (somreadonlyeller?), ændre typen eller noget helt tredje.
Praktiske eksempler
Gør egenskaber skrivebeskyttede
Lad os sige, du har en interface, der repræsenterer en brugerprofil:
interface UserProfile {
name: string;
age: number;
email: string;
}
Du kan oprette en ny type, hvor alle egenskaber er skrivebeskyttede:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Nu vil ReadOnlyUserProfile have de samme egenskaber som UserProfile, men de vil alle være skrivebeskyttede.
Gør egenskaber valgfri
På samme måde kan du gøre alle egenskaber valgfri:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile vil have alle egenskaber fra UserProfile, men hver egenskab vil være valgfri.
Modificering af egenskabstyper
Du kan også modificere typen for hver egenskab. For eksempel kan du transformere alle egenskaber til at være strenge:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
I dette tilfælde vil alle egenskaber i StringifiedUserProfile være af typen string.
Hvad er Conditional Types?
Conditional Types giver dig mulighed for at definere typer, der afhænger af en betingelse. De giver en måde at udtrykke type-relationer på, baseret på om en type opfylder en bestemt begrænsning. Dette ligner en ternær operator i JavaScript, men for typer.
Grundlæggende syntaks
Syntaksen for en Conditional Type er som følger:
T extends U ? X : Y
T: Typen der bliver tjekket.U: Typen somTudvider (betingelsen).X: Typen der returneres, hvisTudviderU(betingelsen er sand).Y: Typen der returneres, hvisTikke udviderU(betingelsen er falsk).
Praktiske eksempler
Afgør om en type er en streng
Lad os oprette en type, der returnerer string, hvis input-typen er en streng, og number ellers:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Udtrækning af type fra en Union
Du kan bruge conditional types til at udtrække en specifik type fra en union-type. For eksempel, for at udtrække ikke-nullable typer:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Her, hvis T er null eller undefined, bliver typen til never, som derefter filtreres fra af TypeScript's forenkling af union-typer.
Inferring af Typer
Conditional types kan også bruges til at udlede (infer) typer ved hjælp af infer nøgleordet. Dette giver dig mulighed for at udtrække en type fra en mere kompleks typestruktur.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
I dette eksempel udtrækker ReturnType returtypen fra en funktion. Den tjekker, om T er en funktion, der tager vilkårlige argumenter og returnerer en type R. Hvis den er det, returnerer den R; ellers returnerer den any.
Kombination af Mapped Types og Conditional Types
Den virkelige styrke ved Mapped Types og Conditional Types kommer fra at kombinere dem. Dette giver dig mulighed for at skabe meget fleksible og udtryksfulde type-transformationer.
Eksempel: Deep Readonly
En almindelig anvendelse er at skabe en type, der gør alle egenskaber i et objekt, inklusiv indlejrede egenskaber, skrivebeskyttede. Dette kan opnås ved hjælp af en rekursiv conditional type.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
Her anvender DeepReadonly rekursivt readonly modifikatoren på alle egenskaber og deres indlejrede egenskaber. Hvis en egenskab er et objekt, kalder den rekursivt DeepReadonly på det objekt. Ellers anvender den blot readonly modifikatoren på egenskaben.
Eksempel: Filtrering af egenskaber efter type
Lad os sige, at du vil oprette en type, der kun inkluderer egenskaber af en bestemt type. Du kan kombinere Mapped Types og Conditional Types for at opnå dette.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
I dette eksempel itererer FilterByType over egenskaberne i T og tjekker, om typen af hver egenskab udvider U. Hvis den gør det, inkluderer den egenskaben i den resulterende type; ellers ekskluderer den den ved at mappe nøglen til never. Bemærk brugen af "as" til at remappe nøgler. Vi bruger derefter `Omit` og `keyof StringProperties` til at fjerne streng-egenskaberne fra den oprindelige interface.
Avancerede anvendelser og mønstre
Ud over de grundlæggende eksempler kan Mapped Types og Conditional Types bruges i mere avancerede scenarier til at skabe meget tilpassede og typesikre applikationer.
Distributive Conditional Types
Conditional types er distributive, når den type, der kontrolleres, er en union-type. Det betyder, at betingelsen anvendes på hvert medlem af unionen individuelt, og resultaterne kombineres derefter til en ny union-type.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
I dette eksempel anvendes ToArray på hvert medlem af unionen string | number individuelt, hvilket resulterer i string[] | number[]. Hvis betingelsen ikke var distributiv, ville resultatet have været (string | number)[].
Brug af Utility Types
TypeScript tilbyder adskillige indbyggede utility-typer, der udnytter Mapped Types og Conditional Types. Disse utility-typer kan bruges som byggeklodser til mere komplekse type-transformationer.
Partial<T>: Gør alle egenskaber iTvalgfrie.Required<T>: Gør alle egenskaber iTpåkrævede.Readonly<T>: Gør alle egenskaber iTskrivebeskyttede.Pick<T, K>: Vælger et sæt egenskaberKfraT.Omit<T, K>: Fjerner et sæt egenskaberKfraT.Record<K, T>: Konstruerer en type med et sæt egenskaberKaf typenT.Exclude<T, U>: Ekskluderer alle typer fraT, der kan tildelesU.Extract<T, U>: Ekstraherer alle typer fraT, der kan tildelesU.NonNullable<T>: EkskluderernullogundefinedfraT.Parameters<T>: Henter parametrene for en funktionstypeT.ReturnType<T>: Henter returtypen for en funktionstypeT.InstanceType<T>: Henter instanstypen for en constructor-funktionstypeT.
Disse utility-typer er kraftfulde værktøjer, der kan forenkle komplekse typemanipulationer. For eksempel kan du kombinere Pick og Partial for at skabe en type, der kun gør visse egenskaber valgfrie:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
I dette eksempel har OptionalDescriptionProduct alle egenskaberne fra Product, men egenskaben description er valgfri.
Brug af Template Literal Types
Template Literal Types giver dig mulighed for at oprette typer baseret på streng-literaler. De kan bruges i kombination med Mapped Types og Conditional Types til at skabe dynamiske og udtryksfulde type-transformationer. For eksempel kan du oprette en type, der præfikser alle egenskabsnavne med en bestemt streng:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
I dette eksempel vil PrefixedSettings have egenskaberne data_apiUrl og data_timeout.
Bedste praksis og overvejelser
- Hold det simpelt: Selvom Mapped Types og Conditional Types er kraftfulde, kan de også gøre din kode mere kompleks. Prøv at holde dine type-transformationer så enkle som muligt.
- Brug Utility Types: Udnyt TypeScript's indbyggede utility-typer, når det er muligt. De er velafprøvede og kan forenkle din kode.
- Dokumenter dine typer: Dokumenter tydeligt dine type-transformationer, især hvis de er komplekse. Dette vil hjælpe andre udviklere med at forstå din kode.
- Test dine typer: Brug TypeScript's typekontrol til at sikre, at dine type-transformationer fungerer som forventet. Du kan skrive enhedstests for at verificere dine typers adfærd.
- Overvej ydeevne: Komplekse type-transformationer kan påvirke ydeevnen af din TypeScript-compiler. Vær opmærksom på kompleksiteten af dine typer og undgå unødvendige beregninger.
Konklusion
Mapped Types og Conditional Types er kraftfulde funktioner i TypeScript, der giver dig mulighed for at skabe meget fleksible og udtryksfulde type-transformationer. Ved at mestre disse koncepter kan du forbedre typesikkerheden, vedligeholdelsen og den overordnede kvalitet af dine TypeScript-applikationer. Fra simple transformationer som at gøre egenskaber valgfrie eller skrivebeskyttede til komplekse rekursive transformationer og betinget logik, giver disse funktioner de værktøjer, du har brug for til at bygge robuste og skalerbare applikationer. Fortsæt med at udforske og eksperimentere med disse funktioner for at frigøre deres fulde potentiale og blive en dygtigere TypeScript-udvikler.
Når du fortsætter din TypeScript-rejse, så husk at udnytte den rigdom af ressourcer, der er tilgængelige, herunder den officielle TypeScript-dokumentation, online fællesskaber og open source-projekter. Omfavn kraften i Mapped Types og Conditional Types, og du vil være godt rustet til at tackle selv de mest udfordrende type-relaterede problemer.